# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

# This module implements a series - a set of data to be plotted on a chart.
#
# This Series is in the database if and only if self->{'series_id'} is defined.
# Note that the series being in the database does not mean that the fields of
# this object are the same as the DB entries, as the object may have been
# altered.

package Bugzilla::Series;

use 5.14.0;
use strict;
use warnings;

use Bugzilla::Error;
use Bugzilla::Util;

# This is a hack so that we can re-use the rename_field_value
# code from Bugzilla::Search::Saved.
use constant DB_TABLE => 'series';
use constant ID_FIELD => 'series_id';

sub new {
  my $invocant = shift;
  my $class = ref($invocant) || $invocant;

  # Create a ref to an empty hash and bless it
  my $self = {};
  bless($self, $class);

  my $arg_count = scalar(@_);

  # new() can return undef if you pass in a series_id and the user doesn't
  # have sufficient permissions. If you create a new series in this way,
  # you need to check for an undef return, and act appropriately.
  my $retval = $self;

  # There are three ways of creating Series objects. Two (CGI and Parameters)
  # are for use when creating a new series. One (Database) is for retrieving
  # information on existing series.
  if ($arg_count == 1) {
    if (ref($_[0])) {

      # We've been given a CGI object to create a new Series from.
      # This series may already exist - external code needs to check
      # before it calls writeToDatabase().
      $self->initFromCGI($_[0]);
    }
    else {
      # We've been given a series_id, which should represent an existing
      # Series.
      $retval = $self->initFromDatabase($_[0]);
    }
  }
  elsif ($arg_count >= 6 && $arg_count <= 8) {

    # We've been given a load of parameters to create a new Series from.
    # Currently, undef is always passed as the first parameter; this allows
    # you to call writeToDatabase() unconditionally.
    # XXX - You cannot set category_id and subcategory_id from here.
    $self->initFromParameters(@_);
  }
  else {
    die("Bad parameters passed in - invalid number of args: $arg_count");
  }

  return $retval;
}

sub initFromDatabase {
  my ($self, $series_id) = @_;
  my $dbh  = Bugzilla->dbh;
  my $user = Bugzilla->user;

  detaint_natural($series_id)
    || ThrowCodeError("invalid_series_id", {'series_id' => $series_id});

  my $grouplist = $user->groups_as_string;

  my @series = $dbh->selectrow_array(
    "SELECT series.series_id, cc1.name, "
      . "cc2.name, series.name, series.creator, series.frequency, "
      . "series.query, series.is_public, series.category, series.subcategory "
      . "FROM series "
      . "INNER JOIN series_categories AS cc1 "
      . "    ON series.category = cc1.id "
      . "INNER JOIN series_categories AS cc2 "
      . "    ON series.subcategory = cc2.id "
      . "LEFT JOIN category_group_map AS cgm "
      . "    ON series.category = cgm.category_id "
      . "    AND cgm.group_id NOT IN($grouplist) "
      . "WHERE series.series_id = ? "
      . "    AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))",
    undef,
    ($series_id, $user->id)
  );

  if (@series) {
    $self->initFromParameters(@series);
    return $self;
  }
  else {
    return undef;
  }
}

sub initFromParameters {

  # Pass undef as the first parameter if you are creating a new series.
  my $self = shift;

  (
    $self->{'series_id'}, $self->{'category'},   $self->{'subcategory'},
    $self->{'name'},      $self->{'creator_id'}, $self->{'frequency'},
    $self->{'query'},     $self->{'public'},     $self->{'category_id'},
    $self->{'subcategory_id'}
  ) = @_;

  # If the first parameter is undefined, check if this series already
  # exists and update it series_id accordingly
  $self->{'series_id'} ||= $self->existsInDatabase();
}

sub initFromCGI {
  my $self = shift;
  my $cgi  = shift;

  $self->{'series_id'} = $cgi->param('series_id') || undef;
  if (defined($self->{'series_id'})) {
    detaint_natural($self->{'series_id'})
      || ThrowCodeError("invalid_series_id", {'series_id' => $self->{'series_id'}});
  }

  $self->{'category'}
    = $cgi->param('category')
    || $cgi->param('newcategory')
    || ThrowUserError("missing_category");

  $self->{'subcategory'}
    = $cgi->param('subcategory')
    || $cgi->param('newsubcategory')
    || ThrowUserError("missing_subcategory");

  $self->{'name'} = $cgi->param('name') || ThrowUserError("missing_name");

  $self->{'creator_id'} = Bugzilla->user->id;

  $self->{'frequency'} = $cgi->param('frequency');
  detaint_natural($self->{'frequency'}) || ThrowUserError("missing_frequency");

  $self->{'query'} = $cgi->canonicalise_query(
    "format",      "ctype", "action",    "category",
    "subcategory", "name",  "frequency", "public",
    "query_format"
  );
  trick_taint($self->{'query'});

  $self->{'public'} = $cgi->param('public') ? 1 : 0;

  # Change 'admin' here and in series.html.tmpl, or remove the check
  # completely, if you want to change who can make series public.
  $self->{'public'} = 0 unless Bugzilla->user->in_group('admin');
}

sub writeToDatabase {
  my $self = shift;

  my $dbh = Bugzilla->dbh;
  $dbh->bz_start_transaction();

  my $category_id    = getCategoryID($self->{'category'});
  my $subcategory_id = getCategoryID($self->{'subcategory'});

  my $exists;
  if ($self->{'series_id'}) {
    $exists = $dbh->selectrow_array(
      "SELECT series_id FROM series
                                   WHERE series_id = $self->{'series_id'}"
    );
  }

  # Is this already in the database?
  if ($exists) {

    # Update existing series
    my $dbh = Bugzilla->dbh;
    $dbh->do(
      "UPDATE series SET "
        . "category = ?, subcategory = ?,"
        . "name = ?, frequency = ?, is_public = ?  "
        . "WHERE series_id = ?",
      undef,
      $category_id,
      $subcategory_id,
      $self->{'name'},
      $self->{'frequency'},
      $self->{'public'},
      $self->{'series_id'}
    );
  }
  else {
    # Insert the new series into the series table
    $dbh->do(
      "INSERT INTO series (creator, category, subcategory, "
        . "name, frequency, query, is_public) VALUES "
        . "(?, ?, ?, ?, ?, ?, ?)",
      undef,
      $self->{'creator_id'},
      $category_id,
      $subcategory_id,
      $self->{'name'},
      $self->{'frequency'},
      $self->{'query'},
      $self->{'public'}
    );

    # Retrieve series_id
    $self->{'series_id'}
      = $dbh->selectrow_array("SELECT MAX(series_id) " . "FROM series");
    $self->{'series_id'}
      || ThrowCodeError("missing_series_id", {'series' => $self});
  }

  $dbh->bz_commit_transaction();
}

# Check whether a series with this name, category and subcategory exists in
# the DB and, if so, returns its series_id.
sub existsInDatabase {
  my $self = shift;
  my $dbh  = Bugzilla->dbh;

  my $category_id    = getCategoryID($self->{'category'});
  my $subcategory_id = getCategoryID($self->{'subcategory'});

  trick_taint($self->{'name'});
  my $series_id
    = $dbh->selectrow_array("SELECT series_id "
      . "FROM series WHERE category = $category_id "
      . "AND subcategory = $subcategory_id AND name = "
      . $dbh->quote($self->{'name'}));

  return ($series_id);
}

# Get a category or subcategory IDs, creating the category if it doesn't exist.
sub getCategoryID {
  my ($category) = @_;
  my $category_id;
  my $dbh = Bugzilla->dbh;

  # This seems for the best idiom for "Do A. Then maybe do B and A again."
  while (1) {

    # We are quoting this to put it in the DB, so we can remove taint
    trick_taint($category);

    $category_id
      = $dbh->selectrow_array("SELECT id "
        . "from series_categories "
        . "WHERE name ="
        . $dbh->quote($category));

    last if defined($category_id);

    $dbh->do("INSERT INTO series_categories (name) "
        . "VALUES ("
        . $dbh->quote($category)
        . ")");
  }

  return $category_id;
}

##########
# Methods
##########
sub id   { return $_[0]->{'series_id'}; }
sub name { return $_[0]->{'name'}; }

sub creator {
  my $self = shift;

  if (!$self->{creator} && $self->{creator_id}) {
    require Bugzilla::User;
    $self->{creator} = new Bugzilla::User($self->{creator_id});
  }
  return $self->{creator};
}

sub remove_from_db {
  my $self = shift;
  my $dbh  = Bugzilla->dbh;

  $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id);
}

1;

=head1 B<Methods in need of POD>

=over

=item creator

=item existsInDatabase

=item name

=item getCategoryID

=item initFromParameters

=item initFromCGI

=item initFromDatabase

=item remove_from_db

=item writeToDatabase

=item id

=back
